<!DOCTYPE html>
<html>

<head>
    <title>terrain test</title>
    <script type="text/javascript" src="https://unpkg.com/dat.gui@0.7.6/build/dat.gui.min.js"></script>
    <link type="text/css" rel="stylesheet" href="https://unpkg.com/maptalks/dist/maptalks.css">
    <script type="text/javascript" src="https://unpkg.com/maptalks/dist/maptalks.js"></script>
    <script type="text/javascript" src="https://unpkg.com/@maptalks/gl/dist/maptalksgl.js"></script>
    <script type="text/javascript" src="./js/accesstoken.js"></script>
    <script type="text/javascript" src="https://unpkg.com/three@0.138.0/build/three.min.js"></script>
    <script type="text/javascript"
        src="https://unpkg.com/maptalks.three@latest/dist/maptalks.three.js"></script>
    <script type="text/javascript" src="https://deyihu.github.io/city-build/js/tile-cover.js"></script>
    <script type="text/javascript"
        src="https://unpkg.com/three@0.109.0/examples/js/libs/stats.min.js"></script>
    <style>
        html,
        body {
            margin: 0px;
            height: 100%;
            width: 100%;
        }

        #map {
            width: 100%;
            height: 100%;
            /* background-color: #000; */
        }
    </style>
</head>

<body>
    <div id="map"></div>
    <script>

        var baseLayer = new maptalks.TileLayer('tile', {
            // urlTemplate: 'https://mt2.google.cn/maps/vt?lyrs=s&hl=zh-CN&gl=CN&x={x}&y={y}&z={z}',
            urlTemplate: `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.webp?sku=101XzrMiclXn4&access_token=${accesstoken}`,
            subdomains: ['1', '2', '3', '4'],
            // debug: true,
            // debugOutline: 'red'
            // attribution: '&copy; <a href="http://osm.org">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/">CARTO</a>'
        });

        baseLayer._getTileExtent = function (x, y, z) {
            const map = this.getMap(),
                res = map._getResolution(z),
                tileConfig = this._getTileConfig(),
                tileExtent = tileConfig.getTilePrjExtent(x, y, res);
            return tileExtent;
        }

        /**
         *
         * @param {} x
         * @param {*} y
         * @param {*} z
         */
        baseLayer._getTileLngLatExtent = function (x, y, z) {
            const tileExtent = this._getTileExtent(x, y, z);
            let max = tileExtent.getMax(),
                min = tileExtent.getMin();
            const map = this.getMap();
            const projection = map.getProjection();
            min = projection.unproject(min);
            max = projection.unproject(max);
            return new maptalks.Extent(min, max);
        }


        var map = new maptalks.Map("map", {
            "center": [119.98872526736125, 30.137171649562305], "zoom": 13.016229111203135, "pitch": 64.80000000000003, "bearing": -0.6000000000000227,
            centerCross: true,
            doubleClickZoom: false,
            baseLayer: baseLayer
        });

        const layer = new maptalks.VectorLayer('layer').addTo(map);

        // the ThreeLayer to draw buildings
        var threeLayer = new maptalks.ThreeLayer('t', {
            forceRenderOnMoving: true,
            forceRenderOnRotating: true,
            forceRenderOnZooming: true,
        });

        const material = new THREE.MeshBasicMaterial({ wireframe: false, color: "#FFF" });
        const lines = [];


        threeLayer.prepareToDraw = function (gl, scene, camera) {
            stats = new Stats();
            stats.domElement.style.zIndex = 100;
            document.getElementById('map').appendChild(stats.domElement);

            var light = new THREE.DirectionalLight(0xffffff);
            light.position.set(0, -10, 10).normalize();
            scene.add(light);
            scene.add(new THREE.AmbientLight(0xffffff, 0.5));

            baseLayer.hide();
            fetch('./data/west-lake-area.geojson').then(res => res.json()).then(geojson => {
                const polygons = maptalks.GeoJSON.toGeometry(geojson);
                const polygon = polygons[0];
                let extent = polygon.getExtent();

                const { xmin, ymin, xmax, ymax } = extent;
                let coords = [
                    [xmin, ymin],
                    [xmin, ymax],
                    [xmax, ymax],
                    [xmax, ymin]
                ];
                let rectangle = new maptalks.Polygon([coords]);
                const tiles = cover.tiles(rectangle.toGeoJSON().geometry, {
                    min_zoom: 12,
                    max_zoom: 12
                });
                console.log(tiles);
                //buffer
                let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
                tiles.forEach(tile => {
                    const [x, y, z] = tile;
                    const { xmin, ymin, xmax, ymax } = baseLayer._getTileLngLatExtent(x, y, z);
                    minx = Math.min(minx, xmin);
                    maxx = Math.max(maxx, xmax);
                    miny = Math.min(miny, ymin);
                    maxy = Math.max(maxy, ymax);
                });
                extent = new maptalks.Extent(minx, miny, maxx, maxy);
                coords = [
                    [minx, miny],
                    [minx, maxy],
                    [maxx, maxy],
                    [maxx, miny]
                ];
                rectangle = new maptalks.Polygon([coords]);
                // layer.addGeometry(rectangle);
                generateCanvas(tiles, function ({ image, width, height, texture }) {
                    const terrain = new Terrain(extent, {
                        texture,
                        imageWidth: Math.ceil(width / 1),
                        imageHeight: Math.ceil(height / 1),
                        image,
                        factor: 1.5,
                        filterIndex: true
                    }, material, threeLayer);
                    lines.push(terrain);
                    threeLayer.addMesh(terrain);
                    animation();
                    initGui();
                });
            })
        }
        const sceneConfig = {
            postProcess: {
                enable: true,
                antialias: { enable: true }
            }
        };
        const groupLayer = new maptalks.GroupGLLayer('group', [threeLayer], { sceneConfig });
        groupLayer.addTo(map);





        function generateCanvas(tiles, callback) {
            let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
            tiles.forEach(tile => {
                const [x, y, z] = tile;
                minx = Math.min(minx, x);
                maxx = Math.max(maxx, x);
                miny = Math.min(miny, y);
                maxy = Math.max(maxy, y);
            });
            // console.log(maxx, minx, maxy, miny);
            const width = (maxx - minx + 1) * 256, height = (maxy - miny + 1) * 256;
            console.log(width, height);
            const images = [];
            tiles.forEach(tile => {
                const [x, y, z] = tile;
                const dx = (x - minx) * 256, dy = (y - miny) * 256;
                images.push({
                    dx, dy, tile
                });
            });
            generateRGBImage(width, height, images, function (opt) {
                generateTexture(width, height, images, function (opt1) {
                    callback && callback(Object.assign(opt, opt1));
                })
            })
        }

        function generateRGBImage(width, height, images, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = width;
            canvas.height = height;
            // document.body.appendChild(canvas);
            const ctx = canvas.getContext('2d');

            let idx = 0;
            function loadImage() {
                const { tile, dx, dy } = images[idx];
                const [x, y, z] = tile;
                const url = `https://a.tiles.mapbox.com/v4/mapbox.terrain-rgb/${z}/${x}/${y}.pngraw?access_token=${accesstoken}`;
                // console.log(url);
                const image = new Image();
                image.src = url;
                image.crossOrigin = 'Anonymous';
                image.onload = function () {
                    ctx.drawImage(image, dx, dy, 256, 256);
                    // console.log(canvas.toDataURL());
                    idx++;
                    if (idx < images.length) {
                        loadImage();
                    } else {
                        // console.log(canvas.toDataURL());
                        callback && callback({
                            image: canvas.toDataURL(),
                            width,
                            height,
                        });
                    }
                }
            }
            loadImage();
        }

        function generateTexture(width, height, images, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = width;
            canvas.height = height;
            // document.body.appendChild(canvas);
            const ctx = canvas.getContext('2d');

            let idx = 0;
            function loadImage() {
                const { tile, dx, dy } = images[idx];
                const [x, y, z] = tile;
                // const url = `http://t1.tianditu.com/DataServer?T=img_w&X=${x}&Y=${y}&L=${z}&tk=de0dc270a51aaca3dd4e64d4f8c81ff6`;
                const url = `https://api.mapbox.com/v4/mapbox.satellite/${z}/${x}/${y}.webp?sku=101XzrMiclXn4&access_token=${accesstoken}`;
                // console.log(url);
                const image = new Image();
                image.src = url;
                image.crossOrigin = 'Anonymous';
                image.onload = function () {
                    ctx.drawImage(image, dx, dy, 256, 256);
                    // console.log(canvas.toDataURL());
                    idx++;
                    if (idx < images.length) {
                        loadImage();
                    } else {
                        // console.log(canvas.toDataURL());
                        callback && callback({
                            texture: canvas.toDataURL(),
                            width,
                            height,
                        });
                    }
                }
            }
            loadImage();
        }

        function animation() {
            // layer animation support Skipping frames
            threeLayer._needsUpdate = !threeLayer._needsUpdate;
            if (threeLayer._needsUpdate && !map.isInteracting()) {
                threeLayer.redraw();
            }
            stats.update();
            requestAnimationFrame(animation);

        }


        function initGui() {
            var params = {
                add: true,
                color: material.color.getStyle(),
                wireframe: material.wireframe,
                show: true,
                opacity: 1,
                altitude: 0,
            };
            var gui = new dat.GUI();
            gui.add(params, 'add').onChange(function () {
                if (params.add) {
                    threeLayer.addMesh(lines);
                } else {
                    threeLayer.removeMesh(lines);
                }
            });
            gui.addColor(params, 'color').name('color').onChange(function () {
                material.color.set(params.color);
                lines.forEach(function (mesh) {
                    mesh.setSymbol(material);
                });
            });
            gui.add(params, 'opacity', 0, 1).onChange(function () {
                material.opacity = params.opacity;
                lines.forEach(function (mesh) {
                    mesh.setSymbol(material);
                });
            });
            gui.add(params, 'show').onChange(function () {
                lines.forEach(function (mesh) {
                    if (params.show) {
                        mesh.show();
                    } else {
                        mesh.hide();
                    }
                });
            });
            gui.add(params, 'wireframe').onChange(function () {
                material.wireframe = params.wireframe;
            });
            gui.add(params, 'altitude', 0, 300).onChange(function () {
                lines.forEach(function (mesh) {
                    mesh.setAltitude(params.altitude);
                });
            });
        }



        const textureLoader = new THREE.TextureLoader();
        const canvas = document.createElement('canvas'), tileSize = 256;

        function getRGBData(image, width = tileSize, height = tileSize) {
            canvas.width = width;
            canvas.height = height;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(image, 0, 0, width, height);
            return ctx.getImageData(0, 0, width, height).data;
        }

        function generateImage(image) {
            if (!image) {
                return null;
            }
            let img;
            if (typeof image === 'string') {
                img = new Image();
                img.src = image;
            } else if (image instanceof HTMLCanvasElement) {
                img = new Image();
                img.src = image.toDataURL();
            } else if (image instanceof Image) {
                img = new Image();
                img.src = image.src;
                img.crossOrigin = image.crossOrigin;
            }
            if (img && !img.crossOrigin) {
                img.crossOrigin = 'Anonymous';
            }
            return img;
        }


        const OPTIONS = {
            interactive: false,
            altitude: 0,
            image: null,
            imageWidth: 256,
            imageHeight: 256,
            texture: null,
            factor: 1,
            filterIndex: false
        };

        /**
         *
         */
        class Terrain extends maptalks.BaseObject {
            constructor(extent, options, material, layer) {
                options = maptalks.Util.extend({}, OPTIONS, options, { layer, extent });
                const { texture, image, altitude, imageHeight, imageWidth, factor, filterIndex } = options;
                if (!image) {
                    console.error('not find image');
                }
                if (!(extent instanceof maptalks.Extent)) {
                    extent = new maptalks.Extent(extent);
                }
                const { xmin, ymin, xmax, ymax } = extent;
                const coords = [
                    [xmin, ymin],
                    [xmin, ymax],
                    [xmax, ymax],
                    [xmax, ymin]
                ];
                let vxmin = Infinity, vymin = Infinity, vxmax = -Infinity, vymax = -Infinity;
                coords.forEach(coord => {
                    const v = layer.coordinateToVector3(coord);
                    const { x, y } = v;
                    vxmin = Math.min(x, vxmin);
                    vymin = Math.min(y, vymin);
                    vxmax = Math.max(x, vxmax);
                    vymax = Math.max(y, vymax);
                });
                const w = Math.abs(vxmax - vxmin), h = Math.abs(vymax - vymin);
                const rgbImg = generateImage(image), img = generateImage(texture);
                const geometry = new THREE.PlaneBufferGeometry(w, h, imageWidth - 1, imageHeight - 1);
                super();
                this._initOptions(options);
                this._createMesh(geometry, material);
                const z = layer.altitudeToVector3(altitude, altitude).x;
                const v = layer.coordinateToVector3(extent.getCenter(), z);
                this.getObject3d().position.copy(v);
                material.transparent = true;
                if (rgbImg) {
                    material.opacity = 0;
                    rgbImg.onload = () => {
                        const width = imageWidth, height = imageHeight;
                        const imgdata = getRGBData(rgbImg, width, height);
                        let idx = 0;
                        let maxZ = -Infinity;
                        //rgb to height  https://docs.mapbox.com/help/troubleshooting/access-elevation-data/
                        for (let i = 0, len = imgdata.length; i < len; i += 4) {
                            const R = imgdata[i], G = imgdata[i + 1], B = imgdata[i + 2];
                            const height = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1);
                            const z = layer.altitudeToVector3(height, height).x * factor;
                            geometry.attributes.position.array[idx * 3 + 2] = z;
                            maxZ = Math.max(z, maxZ);
                            idx++;
                        }
                        this.getOptions().maxZ = maxZ;
                        geometry.attributes.position.needsUpdate = true;
                        if (filterIndex) {
                            const _filterIndex = [];
                            const index = geometry.getIndex().array;
                            const position = geometry.attributes.position.array;
                            const z = maxZ / 15;
                            for (let i = 0, len = index.length; i < len; i += 3) {
                                const a = index[i];
                                const b = index[i + 1];
                                const c = index[i + 2];
                                const z1 = position[a * 3 + 2];
                                const z2 = position[b * 3 + 2];
                                const z3 = position[c * 3 + 2];
                                if (z1 > z || z2 > z || z3 > z) {
                                    _filterIndex.push(a, b, c);
                                }
                            }
                            geometry.setIndex(new THREE.Uint32BufferAttribute(_filterIndex, 1));
                        }
                        if (img) {
                            textureLoader.load(img.src, (texture) => {
                                material.map = texture;
                                material.opacity = 1;
                                material.needsUpdate = true;
                                this.fire('load');
                            });
                        } else {
                            material.opacity = 1;
                        }
                    };
                    rgbImg.onerror = function () {
                        console.error(`not load ${rgbImg.src}`);
                    };
                }
            }
        }

    </script>
</body>

</html>